Skip to content

Enter right-side-assign context for ??= so closures capturing the assigned variable by reference see its assigned type#5900

Merged
staabm merged 6 commits into
phpstan:2.2.xfrom
staabm:5895b
Jun 21, 2026
Merged

Enter right-side-assign context for ??= so closures capturing the assigned variable by reference see its assigned type#5900
staabm merged 6 commits into
phpstan:2.2.xfrom
staabm:5895b

Conversation

@staabm

@staabm staabm commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Summary

When a closure captured a variable by reference and that same variable was the target of a ??= assignment, e.g.

static $isSupported;
$isSupported ??= function (mixed $arg) use (&$isSupported): bool {
    return $isSupported($arg);
};

PHPStan inferred $isSupported as null inside the closure body and reported a false Trying to invoke null but it's not a callable. error. The exact same pattern written with a plain assignment (if (!isset($isSupported)) { $isSupported = function () use (&$isSupported) ... }) worked correctly. This change makes ??= behave like = for this case.

Changes

  • src/Analyser/ExprHandler/AssignOpHandler.php: when evaluating the right-hand side of an Expr\AssignOp\Coalesce (??=) whose target is a simple variable, the handler now calls ExpressionContext::enterRightSideAssign($var->name, $expr->expr), the same call AssignHandler already makes for =/=&.
  • tests/PHPStan/Analyser/nsrt/bug-13810.php: regression test asserting the closure type is inferred for a ??= recursive by-reference closure capture.

Root cause

The by-reference closure-use handling in NodeScopeResolver::processClosureExpr() reads inAssignRightSideVariableName / inAssignRightSideExpr from the current ExpressionContext. When the captured variable matches the variable currently being assigned, it resolves the variable to the assigned (closure) type inside the closure body, which is what makes $x = function () use (&$x) { ... } resolve $x to the closure.

Only AssignHandler (plain = / =&) populated that context. AssignOpHandler, which handles ??= together with the arithmetic/string compound operators, never called enterRightSideAssign(), so for ??= the context stayed empty and the by-reference variable fell back to its pre-assignment type (null after the ??= falsey filter). The fix populates the context for ??=, the only compound operator for which capturing-and-reassigning the same variable by reference is meaningful.

I probed the sibling assignment operators on the same axis: plain =/=& already set the context; the arithmetic and string compound ops (+=, -=, .=, ...) require the left-hand variable to already hold a non-closure value and cannot meaningfully reassign it to a captured closure, so they did not need the change.

Test

  • tests/PHPStan/Analyser/nsrt/bug-13810.php asserts that inside $isSupported ??= function (mixed $arg) use (&$isSupported) { ... } the variable $isSupported is Closure(mixed): bool (previously null), and mixed~null in the enclosing scope after the ??=. Verified to fail before the fix (Actual: null) and pass after.

Fixes phpstan/phpstan#13810

@staabm staabm requested a review from VincentLanglet June 20, 2026 07:03
@staabm

staabm commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

extracted from #5895 in which I did a mistake and mixed in a second unrelated bug

staabm and others added 6 commits June 20, 2026 17:26
…ssigned variable by reference see its assigned type

- `AssignOpHandler` now calls `ExpressionContext::enterRightSideAssign()` when
  evaluating the right-hand side of a `??=` whose target is a simple variable,
  mirroring what `AssignHandler` already does for plain `=`/`=&`.
- This lets the by-reference closure-use handling in `NodeScopeResolver`
  (which inspects `inAssignRightSideVariableName`/`inAssignRightSideExpr`)
  recognise patterns like `$cb ??= function () use (&$cb) { ... }` and resolve
  `$cb` to the assigned closure type inside the closure body instead of `null`.
- Without this, `??=` (unlike `=`) left the right-side-assign context empty, so a
  recursive `use (&$var)` closure saw `$var` as `null` and produced false
  `callable.nonCallable` errors.
- Probed the sibling axis (other assignment operators): plain `=`/`=&` already
  set this context; the arithmetic/string compound ops (`+=`, `.=`, ...) cannot
  meaningfully capture-and-reassign the same variable by reference, so only
  `??=` needed the change.
@staabm staabm merged commit 622006d into phpstan:2.2.x Jun 21, 2026
668 of 672 checks passed
@staabm staabm deleted the 5895b branch June 21, 2026 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

References in use clauses are not processed correctly

2 participants